package com.lucene.service;

import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import com.lucene.handle.TulingMessageHandler;
import com.lucene.model.*;
import com.lucene.utils.Utils;
import com.lucene.handle.MessageHandle;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Created by zhangzhen on 06/10/2017.
 */
public class WechatService {

    private static final Logger logger = LoggerFactory.getLogger(WechatService.class);

    private static WechatService service = null;
    public static WechatService build(Environment environment){
        if (service==null){
            if (environment==null)
                service = new WechatService();
            else
                service = new WechatService(environment);
        }
        return service;
    }

    protected String appid = "wx782c26e4c19acffb";
    protected String wxHost;
    protected String baseUri;
    protected String syncHost;
    // 微信配置信息
    protected Map<String, String> conf = new HashMap<>();
    protected String redirectUri;
    protected String status = null;
    // 登录会话
    protected Session session;
    protected Map<String, Object> baseRequest;
    protected JsonObject synckeyDic;
    protected String     synckey;
    // device_id: 登录手机设备
    // web wechat 的格式为: e123456789012345 (e+15位随机数)
    // mobile wechat 的格式为: A1234567890abcde (A+15位随机数字或字母)
    protected String deviceId = "e" + System.currentTimeMillis();
    protected String userAgent = Const.API_USER_AGENT[new Random().nextInt(2)];
    protected String cookie;
    // 登陆账号信息
    protected Map<String, Object> user;
    // 好友+群聊+公众号+特殊账号
    protected JsonArray memberList;
    protected int memberCount;
    // 好友
    protected JsonArray contactList;
    // 群
    protected JsonArray groupList;
    // 群聊成员字典 {group_id:[]}
    protected Map<String, JsonArray> groupMemeberList = new HashMap<String, JsonArray>();
    // 公众号／服务号
    protected JsonArray publicUsersList;
    // 特殊账号
    protected JsonArray specialUsersList;
    // 读取、连接、发送超时时长，单位/秒
    private int readTimeout, connTimeout, writeTimeout;

    private MessageHandle messageHandle;

    public MessageHandle getMessageHandle() {
        return messageHandle;
    }

    public void setMessageHandle(MessageHandle messageHandle) {
        this.messageHandle = messageHandle;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public JsonArray getContactList() {
        return contactList;
    }

    public void setContactList(JsonArray contactList) {
        this.contactList = contactList;
    }

    private WechatService() {
        this(Environment.of("classpath:application.properties"));
    }

    private WechatService(Environment environment) {
        System.setProperty("https.protocols", "TLSv1");
        System.setProperty("jsse.enableSNIExtension", "false");
        this.connTimeout = environment.getInt("http.conn-time-out", 10);
        this.readTimeout = environment.getInt("http.read-time-out", 10);
        this.writeTimeout = environment.getInt("http.write-time-out", 10);
        this.conf_factory();
    }
    private void conf_factory() {
        conf.put("CONTACTFLAG_CONTACT", "1");
        conf.put("CONTACTFLAG_CHATCONTACT", "2");
        conf.put("CONTACTFLAG_CHATROOMCONTACT", "4");
        conf.put("CONTACTFLAG_BLACKLISTCONTACT", "8");
        conf.put("CONTACTFLAG_DOMAINCONTACT", "16");
        conf.put("CONTACTFLAG_HIDECONTACT", "32");
        conf.put("CONTACTFLAG_FAVOURCONTACT", "64");
        conf.put("CONTACTFLAG_3RDAPPCONTACT", "128");
        conf.put("CONTACTFLAG_SNSBLACKLISTCONTACT", "256");
        conf.put("CONTACTFLAG_NOTIFYCLOSECONTACT", "512");
        conf.put("CONTACTFLAG_TOPCONTACT", "2048");
        conf.put("MSGTYPE_TEXT", "1");
        conf.put("MSGTYPE_IMAGE", "3");
        conf.put("MSGTYPE_VOICE", "34");
        conf.put("MSGTYPE_VIDEO", "43");
        conf.put("MSGTYPE_MICROVIDEO", "62");
        conf.put("MSGTYPE_EMOTICON", "47");
        conf.put("MSGTYPE_APP", "49");
        conf.put("MSGTYPE_VOIPMSG", "50");
        conf.put("MSGTYPE_VOIPNOTIFY", "52");
        conf.put("MSGTYPE_VOIPINVITE", "53");
        conf.put("MSGTYPE_LOCATION", "48");
        conf.put("MSGTYPE_STATUSNOTIFY", "51");
        conf.put("MSGTYPE_SYSNOTICE", "9999");
        conf.put("MSGTYPE_POSSIBLEFRIEND_MSG", "40");
        conf.put("MSGTYPE_VERIFYMSG", "37");
        conf.put("MSGTYPE_SHARECARD", "42");
        conf.put("MSGTYPE_SYS", "10000");
        conf.put("MSGTYPE_RECALLED", "10002");
        conf.put("APPMSGTYPE_TEXT", "1");
        conf.put("APPMSGTYPE_IMG", "2");
        conf.put("APPMSGTYPE_AUDIO", "3");
        conf.put("APPMSGTYPE_VIDEO", "4");
        conf.put("APPMSGTYPE_URL", "5");
        conf.put("APPMSGTYPE_ATTACH", "6");
        conf.put("APPMSGTYPE_OPEN", "7");
        conf.put("APPMSGTYPE_EMOJI", "8");
        conf.put("APPMSGTYPE_VOICE_REMIND", "9");
        conf.put("APPMSGTYPE_SCAN_GOOD", "10");
        conf.put("APPMSGTYPE_GOOD", "13");
        conf.put("APPMSGTYPE_EMOTION", "15");
        conf.put("APPMSGTYPE_CARD_TICKET", "16");
        conf.put("APPMSGTYPE_REALTIME_SHARE_LOCATION", "17");
        conf.put("APPMSGTYPE_TRANSFERS", "2e3");
        conf.put("APPMSGTYPE_RED_ENVELOPES", "2001");
        conf.put("APPMSGTYPE_READER_TYPE", "100001");
        conf.put("UPLOAD_MEDIA_TYPE_IMAGE", "1");
        conf.put("UPLOAD_MEDIA_TYPE_VIDEO", "2");
        conf.put("UPLOAD_MEDIA_TYPE_AUDIO", "3");
        conf.put("UPLOAD_MEDIA_TYPE_ATTACHMENT", "4");
    }

    /**
     * 获取uuid
     *
     * @return
     */
    public boolean getUUID() {
        //  https://login.weixin.qq.com/jslogin
        String url = "https://login.weixin.qq.com/jslogin";
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("appid", appid);
        params.put("fun", "new");
        params.put("lang", "zh_CN");
        params.put("_", System.currentTimeMillis() + "");

        String response = new String(doGet(url,null,params));
        if (Utils.isBlank(response)) {
            logger.warn("获取UUID失败");
            return false;
        }

        String code = Utils.match("window.QRLogin.code = (\\d+);", response);
        if (Utils.isBlank(code)) {
            logger.warn("获取UUID失败");
            return false;
        }

        if (!code.equals("200")) {
            logger.warn("错误的状态码: {}", code);
            return false;
        }
        session = new Session();
        session.setUuid(Utils.match("window.QRLogin.uuid = \"(.*)\";", response));
        return true;
    }
    public byte[] getQrcodeBytes() {
        String url = "https://login.weixin.qq.com/qrcode/" + session.getUuid();
        RequestBody body = new FormBody.Builder()
                .add("t", "webwx")
                .add("_", System.currentTimeMillis() + "").build();
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        try {
            Response response = client.newCall(request).execute();
            byte[] bytes = response.body().bytes();
            return bytes;
        } catch (Exception e) {
            logger.error("[*] 生成二维码失败", e);
        }
        return null;
    }


    /**
     * 生成二维码
     *
     * @return 返回二维码的图片路径
     */
    public String genqrcode() {
        String     url    = "https://login.weixin.qq.com/qrcode/" + session.getUuid();
        final File output = new File("qrcode.jpg");
        logger.info(output.getAbsolutePath());
        RequestBody body = new FormBody.Builder()
                .add("t", "webwx")
                .add("_", System.currentTimeMillis() + "").build();

        System.out.println(url);

        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        try {
            Response response = client.newCall(request).execute();
            byte[] bytes = response.body().bytes();
            FileOutputStream fos = new FileOutputStream(output);
            fos.write(bytes);
            fos.close();
        } catch (Exception e) {
            logger.error("[*] 生成二维码失败", e);
        }
        return output.getPath();
    }
    /**
     * 等待登录
     *      tip=1, 等待用户扫描二维码,
     * 201: scaned
     * 408: timeout
     *      tip=0, 等待用户确认登录,
     * 200: confirmed
     * @return
     */
    public String wait4login() {
        setStatus("waiting");//正在登录
        int MAX_RETRY_TIMES = 8;
        int try_later_secs = 1;
        String code = "unkonwn";
        int tip = 1;
        String url = "https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s";
        int retry_time = MAX_RETRY_TIMES;
        while (retry_time > 0){
            url = String.format(url, tip, session.getUuid(), System.currentTimeMillis());
            String response = new String(doGet(url,null));
            if (!Utils.isBlank(response)) {
                code = Utils.match("window.code=(\\d+);", response);
                if (Utils.isBlank(code)){
                    tip = 1;
                    retry_time -= 1;
                    Utils.sleep(try_later_secs);
                    continue;
                }
                if ("201".equals(code)){
                    logger.info("[*] 请在手机上点击确认以登录 ... \n");
                    logger.info("[*] Please confirm to login .");
                    tip = 0;
                    Utils.sleep(5000);
                }else if("200".equals(code)){
                    String pm = Utils.match("window.redirect_uri=\"(\\S+?)\";", response);
                    String r_uri = pm + "&fun=new";
                    this.redirectUri = r_uri;

                    String parts[] = r_uri.split("://");
                    String httpBodies[] = parts[1].split("/");
                    this.wxHost = httpBodies[0];

                    StringBuffer baseUriBuffer = new StringBuffer(parts[0]);
                    baseUriBuffer.append("://").append(this.wxHost);
                    for (int i=1;i<httpBodies.length-1;i++){
                        baseUriBuffer.append("/").append(httpBodies[i]);
                    }

                    this.baseUri = baseUriBuffer.toString();
                    logger.info("[*] redirectUri : {}",this.redirectUri);
                    logger.info("[*] baseUri : {}",this.baseUri);
                    logger.info("[*] wxHost : {}",this.wxHost);
                    setStatus("waitingok");//登录成功
                    return code;
                }else if ("408".equals(code)){
                    logger.warn("[*] [登陆超时] \n");
                    tip = 1;
                    retry_time -= 1;
                    Utils.sleep(try_later_secs);
                }else {
                    logger.error("[*] WeChat login exception return_code={}. retry in {} secs later...",code, try_later_secs);
                    tip = 1;
                    retry_time -= 1;
                    Utils.sleep(try_later_secs);
                }
            }
        }
        setStatus(null);//登录失败
        return code;
    }

    /**
     * 登录微信
     *
     * @return
     */
    public boolean login() {
        setStatus("logining");
        Request.Builder requestBuilder = new Request.Builder().url(this.redirectUri);
        Request request = requestBuilder.build();

        logger.debug("[*] 请求 => {}\n", request);
        try {
            Response     response = client.newCall(request).execute();
            Headers headers  = response.headers();
            List<String> cookies  = headers.values("Set-Cookie");
            this.cookie = Utils.getCookie(cookies);
            String body = response.body().string();
            if (Utils.isBlank(body)) {
                return false;
            }
            session.setSkey(Utils.match("<skey>(\\S+)</skey>", body));
            session.setSid(Utils.match("<wxsid>(\\S+)</wxsid>", body));
            session.setUin(Utils.match("<wxuin>(\\S+)</wxuin>", body));
            session.setPassTicket(Utils.match("<pass_ticket>(\\S+)</pass_ticket>", body));

            this.baseRequest = Utils.createMap("Uin", Long.valueOf(session.getUin()),
                    "Sid", session.getSid(), "Skey", session.getSkey(), "DeviceID", this.deviceId);
            File output = new File("temp.jpg");
            if (output.exists()) {
                output.delete();
            }
            setStatus("loginok");
            return true;
        } catch (Exception e) {
            logger.error("[*] 登录失败", e);
            setStatus(null);
            return false;
        }
    }

    /**
     * 微信初始化
     *
     * @return
     */
    public boolean init() {
        setStatus("initing");
        if (null == session) {
            return false;
        }
        String url = this.baseUri + "/webwxinit?pass_ticket=%s&skey=%s&r=%s&lang=en_US";
        url = String.format(url, session.getPassTicket(), session.getSkey(), System.currentTimeMillis());
        Map<String, Object> param = Utils.createMap("BaseRequest", this.baseRequest);

        JsonObject response = doPost(url, param).getAsJsonObject();
        if (null == response) {
            return false;
        }
        this.user = new Gson().fromJson(response.get("User"), new TypeToken<Map<String, Object>>() {
        }.getType());
        this.makeSynckey(response);

        JsonObject baseResponse = response.getAsJsonObject("BaseResponse");
        return baseResponse.get("Ret").getAsInt() == 0;
    }

    private void makeSynckey(JsonObject dic) {
        this.synckeyDic = dic.getAsJsonObject("SyncKey");
        StringBuffer synckey = new StringBuffer();
        JsonArray    list    = this.synckeyDic.getAsJsonArray("List");
        for (JsonElement element : list) {
            JsonObject item = element.getAsJsonObject();
            synckey.append("|" + item.get("Key").getAsInt() + "_" + item.get("Val").getAsInt());
        }
        if (synckey.length() > 0) {
            this.synckey = synckey.substring(1);
        }
    }

    /**
     * 开启状态通知
     *
     * @return
     */
    public boolean openStatusNotify() {
        setStatus("openning");
        String url = this.baseUri + "/webwxstatusnotify?lang=zh_CN&pass_ticket=%s";
        url = String.format(url, session.getPassTicket());
        Map<String, Object> params = new HashMap<>();
        params.put("BaseRequest", this.baseRequest);
        params.put("Code", 3);
        params.put("FromUserName", this.user.get("UserName"));
        params.put("ToUserName", this.user.get("UserName"));
        params.put("ClientMsgId", System.currentTimeMillis());

        JsonObject response = doPost(url, params).getAsJsonObject();
        if (null == response) {
            return false;
        }
        JsonObject baseResponse = response.getAsJsonObject("BaseResponse");
        return baseResponse.get("Ret").getAsInt() == 0;
    }

    /**
     * 获取联系人信息
     *
     * @return
     */
    public boolean getContact() {
        String url = this.baseUri + "/webwxgetcontact?seq=0&pass_ticket=%s&skey=%s&r=%s";
        url = String.format(url, session.getPassTicket(), session.getSkey(), System.currentTimeMillis());

        Set<String> specialUsers = Const.API_SPECIAL_USER;

        JsonObject response = doPost(url, null).getAsJsonObject();
        if (null == response) {
            return false;
        }

        this.memberCount = response.get("MemberCount").getAsInt();
        this.memberList = response.getAsJsonArray("MemberList");

        JsonArray ContactList = new JsonArray();
        ContactList.addAll(this.memberList);

        this.publicUsersList = new JsonArray();
        this.groupList = new JsonArray();
        this.specialUsersList = new JsonArray();

        for (JsonElement element : memberList) {

            JsonObject contact = element.getAsJsonObject();
            if (contact.get("VerifyFlag").getAsInt() != 0) { //公众号/服务号
                ContactList.remove(contact);
                this.publicUsersList.add(contact);
            } else if (specialUsers.contains(contact.get("UserName").getAsString())) { //特殊账号
                ContactList.remove(contact);
                this.specialUsersList.add(contact);
            } else if (contact.get("UserName").getAsString().contains("@@")) {// 群聊
                logger.info("[*]group=>"+element.toString());
                ContactList.remove(contact);
                this.groupList.add(contact);
            } else if (contact.get("UserName").getAsString().equals(this.user.get("UserName"))) {// 自己
                ContactList.remove(contact);
            }
        }
        this.contactList = ContactList;
        return true;
    }

    /**
     * 批量获取群成员
     *
     * @param groupIds
     * @return
     */
    public JsonArray batchGetContact(List<String> groupIds) {
        String url = "https://" + this.wxHost + "/cgi-bin/mmwebwx-bin/webwxbatchgetcontact?type=ex&r=%s&pass_ticket=%s";
        url = String.format(url, System.currentTimeMillis(), session.getPassTicket());

        Map<String, Object> params = new HashMap<String, Object>();
        params.put("BaseRequest", this.baseRequest);
        params.put("Count", groupIds.size());

        List<Map> list = new ArrayList<Map>();
        for (String groupId : groupIds) {
            list.add(Utils.createMap("UserName", groupId, "EncryChatRoomId", ""));
        }
        params.put("List", list);

        JsonElement response = doPost(url, params);
        if (null == response) {
            return null;
        }
        JsonObject dic = response.getAsJsonObject();
        return dic.get("ContactList").getAsJsonArray();
    }

    /**
     * 和微信保持同步
     *
     * @return
     */
    public JsonObject wxSync() {
        String url = this.baseUri + "/webwxsync?lang=en_US&sid=%s&skey=%s&pass_ticket=%s";
        url = String.format(url, session.getSid(), session.getSkey(), session.getPassTicket());

        Map<String, Object> params = new HashMap<String, Object>();
        params.put("BaseRequest", this.baseRequest);
        params.put("SyncKey", this.synckeyDic);
        params.put("rr", System.currentTimeMillis());

        JsonElement response = doPost(url, params);
        if (null == response) {
            return null;
        }

        JsonObject dic = response.getAsJsonObject();
        if (null != dic) {
            JsonObject baseResponse = dic.getAsJsonObject("BaseResponse");
            if (null != baseResponse && baseResponse.get("Ret").getAsInt() == 0) {
                this.makeSynckey(dic);
            }
        }
        return dic;
    }

    /**
     * 拉取群成员
     *
     * @return
     */
    public void fetchGroupContacts() {
        logger.debug("fetchGroupContacts");

        List<String> groupIds = new ArrayList<String>();

        Map<String, JsonObject> g_dict = new HashMap<String, JsonObject>();

        for (JsonElement element : groupList) {
            JsonObject group = element.getAsJsonObject();
            groupIds.add(group.get("UserName").getAsString());
            g_dict.put(group.get("UserName").getAsString(), group);
        }

        JsonArray groupMembers = batchGetContact(groupIds);
        for (JsonElement element : groupMembers) {
            JsonObject member_list = element.getAsJsonObject();
            String     g_id        = member_list.get("UserName").getAsString();
            JsonObject group       = g_dict.get(g_id);
            group.addProperty("MemberCount", member_list.get("MemberCount").getAsInt());
            group.addProperty("OwnerUin", member_list.get("OwnerUin").getAsInt());
            this.groupMemeberList.put(g_id, member_list.get("MemberList").getAsJsonArray());
        }
    }

    // TODO
    public boolean snapshot() {
        return false;
    }

    /**
     * 微信同步检查
     *
     * @return
     */
    public int[] synCheck() {
        String url = "https://" + this.syncHost + "/cgi-bin/mmwebwx-bin/synccheck";
        Map<String, Object> params = new HashMap<>();
        params.put("r", System.currentTimeMillis() + Utils.getRandomNumber(5));
        params.put("sid", session.getSid());
        params.put("uin", session.getUin());
        params.put("skey", session.getSkey());
        params.put("deviceid", this.deviceId);
        params.put("synckey", this.synckey);
        params.put("_", System.currentTimeMillis());

        String response = new String(doGet(url, this.cookie, null,params));

        int[] arr = new int[]{-1, -1};
        if (Utils.isBlank(response)) {
            return arr;
        }
        String retcode  = Utils.match("retcode:\"(\\d+)\",", response);
        String selector = Utils.match("selector:\"(\\d+)\"}", response);
        if (null != retcode && null != selector) {
            arr[0] = Integer.parseInt(retcode);
            arr[1] = Integer.parseInt(selector);
        }
        return arr;
    }

    /**
     * 根据用户id和群id查询用户信息
     *
     * @param userId
     * @param groupId
     * @return
     */
    public Map<String, String> getGroupUserById(String userId, String groupId) {
        String unknownPeople = "未知_" + userId;
        Map<String, String> user = new HashMap<String, String>();
        // 微信动态ID
        user.put("UserName", userId);
        // 微信昵称
        user.put("NickName", "");
        // 群聊显示名称
        user.put("DisplayName", "");
        // Log显示用的
        user.put("ShowName", unknownPeople);
        // 群用户id
        user.put("AttrStatus", unknownPeople);

        // 群友
        if (!groupMemeberList.containsKey(groupId)) {
            return user;
        }

        JsonArray memebers = groupMemeberList.get(groupId);
        for (JsonElement element : memebers) {
            JsonObject member = element.getAsJsonObject();
            if (member.get("UserName").getAsString().equals(userId)) {
                logger.info("[*] {} => {}",element,member);
                String n = Utils.emptyOr(member.get("NickName").getAsString(), "");
                String d = Utils.emptyOr(member.get("DisplayName").getAsString(), "");
                user.put("NickName", n);
                user.put("DisplayName", d);
                user.put("AttrStatus", Utils.emptyOr(member.get("AttrStatus").getAsString(), ""));
                user.put("ShowName", Utils.emptyOr(d, n));
                break;
            }
        }
        return user;
    }

    /**
     * 根据群id查询群信息
     *
     * @param groupId
     * @return
     */
    public Map<String, String> getGroupById(String groupId) {
        String unknownGroup = "未知群_" + groupId;
        Map<String, String> group = new HashMap<String, String>();
        group.put("UserName", groupId);
        group.put("NickName", "");
        group.put("DisplayName", "");
        group.put("ShowName", "");
        group.put("OwnerUin", "");
        group.put("MemberCount", "");

        for (JsonElement element : groupList) {
            JsonObject member = element.getAsJsonObject();
            if (member.get("UserName").getAsString().equals(groupId)) {
                group.put("NickName", Utils.emptyOr(member.get("NickName").getAsString(), ""));
                group.put("DisplayName", Utils.emptyOr(member.get("DisplayName").getAsString(), ""));
                group.put("ShowName", Utils.emptyOr(member.get("NickName").getAsString(), ""));
                group.put("OwnerUin", Utils.emptyOr(member.get("OwnerUin").getAsString(), ""));
                group.put("MemberCount", Utils.emptyOr(member.get("MemberCount").getAsString(), "0"));
                break;
            }
        }

        return group;
    }

    /**
     * 根据用户id查询用户信息
     *
     * @param userId
     * @return
     */
    public Map<String, String> getUserById(String userId) {
        String unknownPeople = "未知_" + userId;
        Map<String, String> user          = new HashMap<String, String>();
        user.put("UserName", userId);
        user.put("RemarkName", "");
        user.put("NickName", "");
        user.put("ShowName", unknownPeople);

        if (userId.equals(this.user.get("UserName"))) {
            user.put("RemarkName", this.user.get("RemarkName").toString());
            user.put("NickName", this.user.get("NickName").toString());
            user.put("ShowName", this.user.get("NickName").toString());
        } else {
            // 联系人
            for (JsonElement element : memberList) {
                JsonObject item = element.getAsJsonObject();
                if (item.get("UserName").getAsString().equals(userId)) {
                    user.put("RemarkName", Utils.emptyOr(item.get("RemarkName").getAsString(), ""));
                    user.put("NickName", Utils.emptyOr(item.get("NickName").getAsString(), ""));
                    if (Utils.isNotBlank(user.get("RemarkName"))) {
                        user.put("ShowName", user.get("RemarkName"));
                    } else {
                        user.put("ShowName", user.get("NickName"));
                    }
                    break;
                }
            }
            // 特殊账号
            for (JsonElement element : specialUsersList) {
                JsonObject item = element.getAsJsonObject();
                if (item.get("UserName").getAsString().equals(userId)) {
                    user.put("RemarkName", userId);
                    user.put("NickName", userId);
                    user.put("ShowName", userId);
                    break;
                }
            }
        }
        return user;
    }

    /**
     * 发送微信消息
     *
     * @param msg
     * @param to
     * @return
     */
    public JsonObject wxSendMessage(String msg, String to) {

        String url = "https://" + this.wxHost + "/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=%s";
        url = String.format(url, session.getPassTicket());

        String clientMsgId = System.currentTimeMillis() + Utils.getRandomNumber(5);
        Map<String, Object> params = new HashMap<>();
        params.put("BaseRequest", this.baseRequest);
        Map<String, Object> Msg = new HashMap<>();
        Msg.put("Type", 1);
        Msg.put("Content", Utils.unicodeToUtf8(msg));
        Msg.put("FromUserName", this.user.get("UserName"));
        Msg.put("ToUserName", to);
        Msg.put("LocalID", clientMsgId);
        Msg.put("ClientMsgId", clientMsgId);
        params.put("Msg", Msg);

        JsonElement response = doPost(url, params);
        if (null == response) {
            return null;
        }
        return response.getAsJsonObject();
    }

    /**
     * 发送文本消息
     *
     * @param msg
     * @param uid
     */
    public void sendText(String msg, String uid) {
        this.wxSendMessage(msg, uid);
    }

    public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    private OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(connTimeout, TimeUnit.SECONDS)
            .writeTimeout(writeTimeout, TimeUnit.SECONDS)
            .readTimeout(readTimeout, TimeUnit.SECONDS)
            .build();

    private byte[] doGet(String url,Map<String, String> header, Map<String, Object>... params) {
        return doGet(url, null,header,params);
    }
    private byte[] doGet(String url, String cookie,Map<String, String> header, Map<String, Object>... params) {
        if (null != params && params.length > 0) {
            Map<String, Object> param = params[0];
            Set<String>         keys  = param.keySet();
            StringBuilder       sbuf  = new StringBuilder(url);
            if (url.contains("=")) {
                sbuf.append("&");
            } else {
                sbuf.append("?");
            }
            for (String key : keys) {
                sbuf.append(key).append('=').append(param.get(key)).append('&');
            }
            url = sbuf.substring(0, sbuf.length() - 1);
        }
        try {
            Request.Builder requestBuilder = new Request.Builder().url(url);
            if (null != cookie) {
                requestBuilder.addHeader("Cookie", this.cookie);
                if (header!=null){
                    for (Map.Entry entry:header.entrySet()){
                        requestBuilder.addHeader((String)entry.getKey(), (String)entry.getValue());
                    }
                }
            }

            Request request = requestBuilder.build();

            logger.debug("[*] 请求 => {}\n", request);

            Response response = client.newCall(request).execute();
            byte[]   body     = response.body().bytes();

            logger.debug("[*] 响应 => {}", body);
            return body;
        } catch (Exception e) {
            logger.error("", e);
            return null;
        }
    }

    private JsonElement doPost(String url, Object object) {
        String      bodyJson    = null;
        RequestBody requestBody = RequestBody.create(JSON, "");
        if (null != object) {
            bodyJson = Utils.toJson(object);
            requestBody = RequestBody.create(JSON, bodyJson);
        }

        Request.Builder requestBuilder = new Request.Builder().url(url).post(requestBody);
        if (null != cookie) {
            requestBuilder.addHeader("Cookie", cookie);
        }

        Request request = requestBuilder.build();

        logger.debug("[*] 请求 => {}\n", request);
        try {
            Response response = client.newCall(request).execute();
            String   body     = response.body().string();
            if (null != body && body.length() <= 300) {
                logger.debug("[*] 响应 => {}", body);
            }
            return new JsonParser().parse(body);
        } catch (Exception e) {
            logger.error("", e);
            return new JsonParser().parse("{}");
        }
    }

    public boolean testSynCheck(){
        String [] hs = {"webpush.","webpush."};//webpush2.
        try {
            for (String h : hs){
                this.syncHost = h + this.wxHost;
                int[] checkResponse = synCheck();
                if (checkResponse != null && checkResponse[0]==0){
                    return true;
                }
            }
        }catch (Exception e){
            logger.info("[*] 微信测试接口异常",e.getMessage());
        }
        return false;
    }

    private void processMsg(){
        boolean trs = testSynCheck();
        this.status = "loginsuccess";
        logger.info("[*] current syn check result: {}",trs);
        while (true){
            if("wait4loginout".equals(this.status)){
                return;
            }
            long checkTime = System.currentTimeMillis();
            try {
                int[] checkResponse = synCheck();
                int retcode = checkResponse[0];
                int selector = checkResponse[1];
                logger.debug("[*] retcode: {}, selector: {}", retcode, selector);
                if (retcode == 0){
                    if (selector == 0){
                        Utils.sleep(1000);
                    }else if (selector == 2){//新消息
                        JsonObject dic = wxSync();
                        if (null != dic) {
                            handle_msg(dic);
                        }
                    }else if (selector == 3){//未知消息
                        JsonObject dic = wxSync();
                        if (null != dic) {
                            handle_msg(dic);
                        }
                    }else if (selector == 4){//通讯录更新
                        JsonObject dic = wxSync();
                        if (null != dic) {
                            getContact();
                        }
                    }else if (selector == 6){//可能是红包
                        JsonObject dic = wxSync();
                        if (null != dic) {
                            handle_msg(dic);
                        }
                    }else if (selector == 7){//在手机上操作了微信
                        JsonObject dic = wxSync();
                        if (null != dic) {
                            handle_msg(dic);
                        }
                    }else {
                        logger.info("[*] synheck retcode: {}, selector: {}", retcode, selector);
                        JsonObject dic = wxSync();
                        if (null != dic) {
                            handle_msg(dic);
                        }
                        break;
                    }

                }else if (retcode == 1100){
                    logger.info("[*] 从微信客户端上登出了微信\n");
                    break;
                }else if(retcode == 1101){
                    logger.info("[*] 你在其他地方登录了 WEB 版微信\n");
                    break;
                }else if (retcode == 1102){
                    logger.info("[*] 你在手机上主动退出了\n");
                    break;
                }else {
                    logger.info("[*] synCheck : {} {}\n", retcode, selector);
                    Utils.sleep(10);
                }
                schedule();
            } catch (Exception e){
                logger.info("[*] Excepet in process Message\n");
                e.printStackTrace();
            }
            checkTime = System.currentTimeMillis()-checkTime;
            if (checkTime<0.8)
                Utils.sleep(1-checkTime);
        }
    }
    private void schedule(){
        //@TODO
        /**
         * 做任务型事情的函数，如果需要，可以在子类中覆盖此函数
         * 此函数在处理消息的间隙被调用，请不要长时间阻塞此函数
         **/
    }

    private String get_msg_img_url(String msgid){
        String url = this.baseUri + "/webwxgetmsgimg?MsgID=%s&skey=%s";
        url = String.format(url,msgid, session.getSkey());
        return url;
    }

    private void get_msg_img(String msgid){
        String url = this.baseUri + "/webwxgetmsgimg?MsgID=%s&skey=%s";
        url = String.format(url,msgid, session.getSkey());
        byte[] contents = doGet(url,null);
        logger.info(contents.toString());
        String iname = "img_" + msgid + ".jpg";
        Utils.saveFile("image/"+iname,contents);
    }

    private String get_voice_url(String msgid){
        String url = this.baseUri + "/webwxgetvoice?msgid=%s&skey=%s";
        url = String.format(url,msgid, session.getSkey());
        return url;
    }

    private void get_voice(String msgid){
        String url = this.baseUri + "/webwxgetvoice?msgid=%s&skey=%s";
        url = String.format(url,msgid, session.getSkey());
        byte[] contents = doGet(url,null);
        String iname = "voi_" + msgid + ".mp3";
        Utils.saveFile("voice/"+iname, contents);
    }

    private String get_video_url(String msgid){
        String url = this.baseUri + "/webwxgetvideo?msgid=%s&skey=%s";
        url = String.format(url,msgid, session.getSkey());
        return url;
    }

    private void get_video(String msgid){
        String url = this.baseUri + "/webwxgetvideo?msgid=%s&skey=%s";
        url = String.format(url,msgid, session.getSkey());
        Map header = new HashMap();
        header.put("Range","bytes=0-");
        byte[] contents = doGet(url,header);

        String iname = "vid_" + msgid + ".mp4";
        Utils.saveFile("video/"+iname,contents);
    }

    private void handle_mod(JsonObject dic) {
        logger.debug("handle modify");
        handle_msg(dic);
        JsonArray modContactList = dic.getAsJsonArray("ModContactList");
        for (JsonElement element : modContactList) {
            JsonObject m = element.getAsJsonObject();
            if (m.get("UserName").getAsString().startsWith("@@")) {
                boolean in_list = false;
                String  g_id    = m.get("UserName").getAsString();
                for (JsonElement ge : groupList) {
                    JsonObject group = ge.getAsJsonObject();
                    if (g_id.equals(group.get("UserName").getAsString())) {
                        in_list = true;
                        group.addProperty("MemberCount", m.get("MemberCount").getAsInt());
                        group.addProperty("NickName", m.get("NickName").getAsInt());
                        this.groupMemeberList.put(g_id, m.get("MemberList").getAsJsonArray());
                        if (null != messageHandle) {
                            messageHandle.groupMemberChange(g_id, m.get("MemberList").getAsJsonArray());
                        }
                        break;
                    }
                }
                if (!in_list) {
                    this.groupList.add(m);
                    this.groupMemeberList.put(g_id, m.get("MemberList").getAsJsonArray());
                    if (null != messageHandle) {
                        messageHandle.groupListChange(g_id, m.get("MemberList").getAsJsonArray());
                        messageHandle.groupMemberChange(g_id, m.get("MemberList").getAsJsonArray());
                    }
                }
            } else if (m.get("UserName").getAsString().equals("@")) {
                boolean in_list = false;
                for (JsonElement ue : memberList) {
                    JsonObject u    = ue.getAsJsonObject();
                    String     u_id = m.get("UserName").getAsString();
                    if (u_id.equals(u.get("UserName").getAsString())) {
                        u = m;
                        in_list = true;
                        break;
                    }
                }
                if (!in_list) {
                    this.memberList.add(m);
                }
            }
        }
    }

    public void handle_msg(JsonObject dic) {

        if (null != messageHandle) {
            messageHandle.wxSync(dic);
        }

        JsonArray msgs = dic.getAsJsonArray("AddMsgList");
        if (msgs.size() == 0) {
            return;
        }
        logger.debug(">>> [{}] 条新消息\n", msgs.size());
        for (JsonElement element : msgs) {
            JsonObject msg = element.getAsJsonObject();

            String msgType = msg.get("MsgType").getAsString();
            String msgId = msg.get("MsgId").getAsString();
            String content = msg.get("Content").getAsString().replace("&lt;", "<").replace("&gt;", ">");
            UserMessage userMessage = new UserMessage(this);
            userMessage.setRawMsg(msg);
            userMessage.setMsgId(msgId);

            // 文本groupMessage  msgType : 1
            if (conf.get("MSGTYPE_TEXT").equals(msgType)) {
                // 地理位置消息
                if (content.contains("pictype=location")) {
                    String location = content.split("<br/>")[1];
                    userMessage.setLocation(location);
                    userMessage.setLog(String.format("[位置] %s", location));
                } else {
                    // 普通文本
                    String text = null;
                    if (content.contains(":<br/>")) {
                        text = content.split(":<br/>")[1];
                    } else {
                        text = content;
                    }
                    userMessage.setText(text);
                    userMessage.setLog(text.replace("<br/>", "\n"));
                }
            }else if (conf.get("MSGTYPE_IMAGE").equals(msgType)){
                logger.info("[*] 图片信息");
                get_msg_img(msgId);
                logger.info("[*] 等待回复图片信息.");

            } else if (conf.get("MSGTYPE_VOICE").equals(msgType)){
                logger.info("[*] 语音信息");
                get_voice(msgId);
                logger.info("[*] 等待回复语音信息.");

            }else if (conf.get("MSGTYPE_VIDEO").equals(msgType)){
                logger.info("[*] 视频信息");
                get_video(msgId);
                logger.info("[*] 等待回复视频信息.");

            }else if (conf.get("MSGTYPE_STATUSNOTIFY").equals(msgType)) {//msgType:51
                logger.info("[*] 提示手机网页版微信登录状态\n");
                return;
            }

            this.show_msg(userMessage);

            boolean isGroupMsg = (msg.get("FromUserName").getAsString() + msg.get("ToUserName").getAsString()).contains("@@");
            if (isGroupMsg) {
                GroupMessage groupMessage = make_group_msg(userMessage);
                if (null != messageHandle) {
                    messageHandle.groupMessage(groupMessage);
                }
            } else {
                if (null != messageHandle) {
                    messageHandle.userMessage(userMessage);
                }
            }
        }
    }

    private void show_msg(UserMessage userMessage) {

        Map<String, Object> src = null;
        Map<String, Object> dst = null;
        Map<String, String> group = null;
        JsonObject msg = userMessage.getRawMsg();

        String content = msg.get("Content").getAsString();
        content = content.replace("&lt;", "<").replace("&gt;", ">");
        String msg_id = msg.get("MsgId").getAsString();

        // 接收到来自群的消息
        if (msg.get("FromUserName").getAsString().substring(2).equals("@@")) {
            String groupId = msg.get("FromUserName").getAsString();
            group = this.getGroupById(groupId);
            if (content.contains(":<br/>")) {
                String u_id = content.split(":<br/>")[0];
                src = new HashMap<String, Object>(this.getGroupUserById(u_id, groupId));
                dst = Utils.createMap("ShowName", "GROUP");
            } else {
                String u_id = msg.get("ToUserName").getAsString();
                src = new HashMap(Utils.createMap("ShowName", "SYSTEM"));
                dst = new HashMap(getGroupUserById(u_id, groupId));
            }
        } else {
            // 非群聊消息
            src = new HashMap(this.getUserById(msg.get("FromUserName").getAsString()));
            dst = new HashMap(this.getUserById(msg.get("ToUserName").getAsString()));
        }
        if (null != group) {
            logger.info("[*] {} |{}| {} -> {}: {}\n", msg_id, group.get("ShowName"),
                    dst.get("ShowName"), userMessage.getLog());
        } else {
            logger.info("[*] {} {} -> {}: {}\n", msg_id, src.get("ShowName"),
                    dst.get("ShowName"), userMessage.getLog());
        }
    }
    private GroupMessage make_group_msg(UserMessage userMessage) {
        logger.info("[*] makeGroupMsg {}",userMessage);
        GroupMessage groupMessage = new GroupMessage(this);
        groupMessage.setRawMsg(userMessage.getRawMsg());
        groupMessage.setMsgId(userMessage.getRawMsg().get("MsgId").getAsString());
        groupMessage.setFromUserName(userMessage.getRawMsg().get("FromUserName").getAsString());
        groupMessage.setToUserName(userMessage.getRawMsg().get("ToUserName").getAsString());
        groupMessage.setMsgType(userMessage.getRawMsg().get("MsgType").getAsString());
        groupMessage.setText(userMessage.getText());
        String content = userMessage.getRawMsg().get("Content").getAsString().replace("&lt;", "<").replace("&gt;", ">");
        Map<String, String> group = null, src = null;

        if (groupMessage.getFromUserName().startsWith("@@")) {
            //接收到来自群的消息
            String g_id = groupMessage.getFromUserName();
            groupMessage.setGroupId(g_id);
            group = this.getGroupById(g_id);
            if (content.contains(":<br/>")) {
                // 需要判断是否at自己
                String[] items = content.split(":<br/>");
                String u_id = items[0];
                src = getGroupUserById(u_id, g_id);
                //增加是否at litemind
                if (items.length>2 && (items[1].contains("@litemind"))){
                    groupMessage.setAt(true);
                }else {
                    groupMessage.setAt(false);
                }
            }
        } else if (groupMessage.getToUserName().startsWith("@@")) {
            // 自己发给群的消息
            String g_id = groupMessage.getToUserName();
            groupMessage.setGroupId(g_id);
            String u_id = groupMessage.getFromUserName();
            src = this.getGroupUserById(u_id, g_id);
            group = this.getGroupById(g_id);
        }
        if (null != src) {
            groupMessage.setUser_attrstatus(src.get("AttrStatus"));
            groupMessage.setUser_display_name(src.get("DisplayName"));
            groupMessage.setUser_nickname(src.get("NickName"));
        }
        if (null != group) {
            groupMessage.setGroup_count(group.get("MemberCount"));
            groupMessage.setGroup_owner_uin(group.get("OwnerUin"));
            groupMessage.setGroup_name(group.get("ShowName"));
        }
        groupMessage.setTimestamp(userMessage.getRawMsg().get("CreateTime").getAsString());
        return groupMessage;
    }
    /*
     * 运行服务
     */
    public void run(){
        try {
            //3. 登录操作及相关消息监测
            String result = wait4login();
            if (!"200".equals(result)){
                logger.info("[*] 微信登录失败. 代码：{}\n",result);
                this.status = "loginout";
                return;
            }
            if (login()){
                logger.info("[*] 微信登录成功.\n");
            }else {
                logger.info("[*] 微信登录失败.\n");
                this.status = "loginout";
                return;
            }

            if (init()){
                logger.info("[*] 微信初始化成功.\n");
            }else {
                logger.info("[*] 微信初始化失败.\n");
                this.status = "loginout";
                return;
            }
            openStatusNotify();
            if (getContact()){
                logger.info("[*] 获取 {} 联系人.\n", contactList.size());
                logger.info("[*] 开始处理消息.\n");
            }
            setMessageHandle(new TulingMessageHandler(Environment.of("classpath:application.properties")));
            processMsg();
            this.status = "loginout";

        }catch (UnknownHostException e){
            logger.error("[*] 未知主机异常 -> {}.\n",e.getMessage());
        }catch (Exception e){
            logger.error("[*] 微信网页版运行异常 -> {}.\n",e.getMessage());
            this.status = "loginout";
        }
    }
}
